#include <stdio.h>
#include <stdlib.h> 
#include <sys/time.h>
#include <math.h>
#include <GLUT/glut.h> 
#include <Accelerate/Accelerate.h>

#define kWindowWidth      800 
#define kWindowHeight     800
#define animateSteps      100
// making these power of 2 let
// us perform % with mask instead
#define OUTERLOOP         (0x100000)
#define INNERLOOP         (8)
#define VBSIZE            (0x20000)
#define VBITER            ((OUTERLOOP*INNERLOOP)/VBSIZE)

#define PI 3.14159265

float *vertexArray = NULL;
GLuint VertexBuffer[2][VBITER];

float a,b,c,d;
float a0, b0, c0, d0;
float deltaA, deltaB, deltaC, deltaD;

const vFloat vhalf = (vFloat){0.5f, 0.5f, 0.5f, 0.5f};
const vFloat vrandmax = (vFloat){(float)RAND_MAX, (float)RAND_MAX, (float)RAND_MAX, (float)RAND_MAX};
const vFloat vmax = (vFloat){2.f, 2.f, 2.f, 2.f};
const vFloat vmin = (vFloat){-2.f, -2.f, -2.f, -2.f};

inline float random(float min, float max)
{
  return (float)rand() / (float)RAND_MAX * (float)(max-min) + (float)min;
}

void setRandom()
{
  a = random(0, PI);
  b = random(0, PI);
  c = random(0, PI);
  d = random(0, PI);
}

void keyboard(unsigned char key, int x, int y)
{
  if (key == 27)
    glutPositionWindow(100, 100);  
}

void display()
{
  static timeval t0 = {0};
  timeval t1;
  gettimeofday(&t1, NULL);
  if (t0.tv_sec != 0)
  {
    printf("%f\n", (double)(t1.tv_sec-t0.tv_sec)+(double)(t1.tv_usec-t0.tv_usec)/1000000);
  }
  t0 = t1;

  static unsigned int times = 0;

  if (times == animateSteps)
  {
    times = 0;
  }

  if (times == 0)
  {
    a0 = a;
    b0 = b;
    c0 = c;
    d0 = d;

    setRandom();

    deltaA = (a - a0)/animateSteps;
    deltaB = (b - b0)/animateSteps;
    deltaC = (c - c0)/animateSteps;
    deltaD = (d - d0)/animateSteps;

    a = a0;
    b = b0;
    c = c0;
    d = d0;
  }

  glClear(GL_COLOR_BUFFER_BIT);

  vFloat va = (vFloat){a,a,a,a};
  vFloat vb = (vFloat){b,b,b,b};
  vFloat vc = (vFloat){c,c,c,c};
  vFloat vd = (vFloat){d,d,d,d};

  static int vbo_toggle = 0;
  
  for (int vbiter=0; vbiter<VBITER; vbiter++)
  {
    // Map VBO
    glBindBuffer(GL_ARRAY_BUFFER, VertexBuffer[vbo_toggle][vbiter]);
    glEnableClientState(GL_VERTEX_ARRAY);
    glVertexPointer(2, GL_FLOAT, 0, NULL);
    vertexArray = (float *)glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY); 
    unsigned int vb_index = 0;

    for (int i=vbiter*OUTERLOOP/VBITER; i<(vbiter+1)*OUTERLOOP/VBITER; i+=4)
    {
      vFloat vx = (vFloat){rand(), rand(), rand(), rand()};
      vFloat vy = (vFloat){rand(), rand(), rand(), rand()};
      vFloat x = vx / vrandmax * (vmax - vmin) + vmin;
      vFloat y = vy / vrandmax * (vmax - vmin) + vmin;
      
      for (int j=0; j<INNERLOOP; j++)
      {
        vFloat xn = vsinf(y * va) - vcosf(x * vb);
        vFloat yn = vsinf(x * vc) - vcosf(y * vd);

        x = xn;
        y = yn;
        
        vFloat halfx = x * vhalf;
        vFloat halfy = y * vhalf;
        
        vertexArray[((vb_index+0)<<1)+0] = ((float *)&halfx)[0];
        vertexArray[((vb_index+0)<<1)+1] = ((float *)&halfy)[0];
        vertexArray[((vb_index+1)<<1)+0] = ((float *)&halfx)[1];
        vertexArray[((vb_index+1)<<1)+1] = ((float *)&halfy)[1];
        vertexArray[((vb_index+2)<<1)+0] = ((float *)&halfx)[2];
        vertexArray[((vb_index+2)<<1)+1] = ((float *)&halfy)[2];
        vertexArray[((vb_index+3)<<1)+0] = ((float *)&halfx)[3];
        vertexArray[((vb_index+3)<<1)+1] = ((float *)&halfy)[3];

        vb_index += 4;
      }
    }

    glUnmapBuffer(GL_ARRAY_BUFFER);  
    glDrawArrays(GL_POINTS, 0, VBSIZE);
    // kick off the GPU before we hog up the CPU for next iteration
    glFlush();
  }

  glutSwapBuffers();

  a += deltaA;
  b += deltaB;
  c += deltaC;
  d += deltaD;

  times++;
  vbo_toggle = !vbo_toggle;
} 

void graphicsSetup(void)
{
  float subColor = 1.0f/255;
  
  // clear and set color
  glClearColor(1, 1, 1, 1);
  glClear(GL_COLOR_BUFFER_BIT);
  glColor4f(subColor, subColor, subColor, 0);

  // setup alpha
  glEnable(GL_BLEND);
  glBlendEquation(GL_FUNC_REVERSE_SUBTRACT);
  glBlendFunc(GL_ONE, GL_ONE);

  // setup point render params
  // default point params work well!
  // glPointSize(1);

  // load identity
  glLoadIdentity();

  // generate VBO names and buffers
  for (int i=0; i<2; i++)
  {
    glGenBuffers(VBITER, VertexBuffer[i]);
    
    for (int vbiter=0; vbiter<VBITER; vbiter++)
    {
      // Create and map VBO
      glBindBuffer(GL_ARRAY_BUFFER, VertexBuffer[i][vbiter]);
      glBufferData(GL_ARRAY_BUFFER, 2*VBSIZE*sizeof(float), NULL, GL_DYNAMIC_DRAW);
    }
  }
}

int main(int argc, char** argv) 
{
  //setup initial values
  srand( time(NULL) );
  
  glutInit(&argc, argv); 
  glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB); 
  glutInitWindowSize(kWindowWidth, kWindowHeight); 
  glutInitWindowPosition(100, 100); 
  glutCreateWindow("Peter De Jong");

  //glutFullScreen();
  
  graphicsSetup();

  setRandom();

  glutKeyboardFunc(keyboard);

  glutDisplayFunc(display);

  glutIdleFunc(display);

  glutMainLoop();

  return 0;
}
